package main.java.DMDE.controllers;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.xml.parsers.ParserConfigurationException;

import org.w3c.dom.Document;
import org.w3c.dom.Element;

import com.mxgraph.model.mxCell;
import com.mxgraph.model.mxIGraphModel;
import com.mxgraph.util.mxEvent;
import com.mxgraph.util.mxEventObject;
import com.mxgraph.util.mxEventSource.mxIEventListener;
import com.mxgraph.view.mxCellState;
import com.mxgraph.view.mxGraph;

import main.java.DMDE.controllers.listeners.IDiagramEventListener;
import main.java.DMDE.infrastructure.IProjectContext;
import main.java.DMDE.controllers.IClassController;
import main.java.DMDE.controllers.factories.IClassControllerFactory;
import main.java.DMDE.controllers.factories.IInterfaceControllerFactory;
import main.java.DMDE.controllers.factories.IRelationshipControllerFactory;
import main.java.DMDE.controllers.IRelationshipController;
import main.java.DMDE.model.Attribute;
import main.java.DMDE.model.Class;
import main.java.DMDE.model.Diagram;
import main.java.DMDE.model.Method;
import main.java.DMDE.model.relationships.Relationship;
import main.java.DMDE.model.relationships.RelationshipComponent;
import main.java.DMDE.model.relationships.RelationshipTypes;
import main.java.DMDE.view.CustomGraph;
import main.java.DMDE.view.IDiagramView;
import main.java.DMDE.styling.StyleConstants;
import main.java.DMDE.model.Entity;
import main.java.ERMDE.persistence.IGraphPersistenceService;
import main.java.DMDE.model.Interface;
import main.java.ERMDE.styling.Styler;
import main.java.common.IXmlFileManager;
import main.java.DMDE.controllers.IInterfaceController;
import main.java.DMDE.persistence.DiagramXmlManager;

public class DiagramController extends BaseController implements
	IDiagramController, mxIEventListener {

    public static class CellConstants {
	public static final String ClassPrefix = "Class";
	public static final String MethodPrefix = "Method";
	public static final String AttributePrefix = "Attribute";
	public static final String RelationshipConnectorPrefix = "Relationship";
	public static final String InterfacePrefix = "Interface";
    }

    private enum Operations {
	None, CreateClass, UpdateClass, CreateInterface, UpdateInterface, CreateRelationship, UpdateRelationship, CreateHierarchy, UpdateHierarchy
    }

    private List<IDiagramEventListener> listeners;
    private IDiagramView diagramView;
    private Diagram diagram;
    private Class pendingClass;
    private Interface pendingInterface;
    private Operations currentOperation;
    private IClassControllerFactory classControllerFactory;
    private IInterfaceControllerFactory interfaceControllerFactory;
    private IRelationshipControllerFactory relatControllerFactory;
    private IXmlFileManager xmlFileManager;
    private DiagramXmlManager diagramXmlManager;
	private IGraphPersistenceService graphPersistenceService;

    private CustomGraph graph;
    private List<mxCell> selectedCells;
    private Map<String, mxCell> classCells;
    private Map<String, mxCell> attributeCells;
    private Map<String, mxCell> methodCells;
    private Map<String, mxCell> relationshipConectorCells;

    public DiagramController(IProjectContext projectContext,
	    IDiagramView diagramView,
	    IClassControllerFactory classControllerFactory,
	    IInterfaceControllerFactory interfaceControllerFactory,
	    IRelationshipControllerFactory relatControllerFactory,
	    IXmlFileManager xmlFileManager,
	    IGraphPersistenceService graphPersistenceService) {

	super(projectContext);
	this.graph = new CustomGraph();
	this.graph.getSelectionModel().addListener(mxEvent.CHANGE, this);
	this.diagram = new Diagram();
	this.diagramView = diagramView;
	this.diagramView.setController(this);
	this.listeners = new ArrayList<IDiagramEventListener>();
	this.classControllerFactory = classControllerFactory;
	this.interfaceControllerFactory = interfaceControllerFactory;
	this.relatControllerFactory = relatControllerFactory;
	this.selectedCells = new ArrayList<mxCell>();
	this.currentOperation = Operations.None;
	this.classCells = new HashMap<String, mxCell>();
	this.attributeCells = new HashMap<String, mxCell>();
	this.methodCells = new HashMap<String, mxCell>();
	this.relationshipConectorCells = new HashMap<String, mxCell>();
	this.xmlFileManager = xmlFileManager;
	this.diagramXmlManager = new DiagramXmlManager();
	this.graphPersistenceService = graphPersistenceService;
    }

    @Override
    public IDiagramView getView() {
	return this.diagramView;
    }

    @Override
    public void addListener(IDiagramEventListener listener) {
	this.listeners.add(listener);
    }

    @Override
    public Diagram getDiagram() {
	return diagram;
    }

    @Override
    public mxGraph getGraph() {
	return this.graph;
    }

    @Override
    public boolean hasPendingClass() {
	return this.pendingClass != null;
    }

    @Override
    public void addClass(int x, int y) throws Exception {
	if (pendingClass == null) {
	    return;
	}
	this.graph.getModel().beginUpdate();
	Object parent = this.graph.getDefaultParent();
	try {
	    mxCell classCell = this.addClassToGraph(this.pendingClass, parent, x, y);
	    this.addAttributesToGraph(classCell, this.pendingClass.getAttributes(),
		    this.pendingClass.getId(), StyleConstants.ATTRIBUTE_HEIGHT);
	    this.addMethodToGraph(classCell, this.pendingClass.getMethods(),
		    this.pendingClass.getId());
	} finally {
	    this.diagram.getEntities().add(this.pendingClass);
	    // this.diagram.isNotValidated();
	    for (IDiagramEventListener listener : this.listeners) {
		listener.handleClassAdded(this.diagram, this.pendingClass);
	    }
	    this.graph.getModel().endUpdate();
	}
	this.pendingClass = null;
    }

    private void addMethodToGraph(mxCell parentCell, List<Method> methods,
	    UUID ownerId) {
	String methodId = ownerId.toString();
	String methodName = "";
	for (Method method : methods)
	    methodName += method.toString() + " \n";

	mxCell methodCell = (mxCell) this.graph.insertVertex(parentCell, null,
		methodName, 0, StyleConstants.NAME_HEIGHT
			+ StyleConstants.METHOD_HEIGHT,
		StyleConstants.ENTITY_WIDTH, StyleConstants.METHOD_HEIGHT,
		StyleConstants.METHOD_CLASS_STYLE);

	this.methodCells.put(CellConstants.AttributePrefix + methodId,
		methodCell);

    }

    private void addAttributesToGraph(mxCell parentCell,
	    List<Attribute> attributes, UUID ownerId, double attribHeight) {

	String attributesId = ownerId.toString();
	String attributesName = "";
	if(attributes != null){
	    for (Attribute attrib : attributes)
		attributesName += attrib.toString() + " \n";
	}
	mxCell attributeCell = (mxCell) this.graph.insertVertex(parentCell,
		null, attributesName, 0, StyleConstants.NAME_HEIGHT , StyleConstants.ENTITY_WIDTH,
		attribHeight,
		StyleConstants.ATTRIBUTE_CLASS_STYLE);

	this.attributeCells.put(CellConstants.AttributePrefix + attributesId,
		attributeCell);
    }

    private mxCell addClassToGraph(Class pendClass, Object parent, int x, int y)
	    throws Exception {

	mxCell classCell = (mxCell) this.graph.insertVertex(parent,
		CellConstants.ClassPrefix + pendClass.getId().toString(),
		pendClass.getName(), x, y, StyleConstants.ENTITY_WIDTH,
		StyleConstants.ENTITY_HEIGHT, StyleConstants.CLASS_STYLE);

	this.classCells.put(CellConstants.ClassPrefix
		+ pendClass.getId().toString(), classCell);

	return classCell;
    }

    @Override
    public void createRelationship() {
	this.currentOperation = Operations.CreateRelationship;
	IRelationshipController relationshipController = this.relatControllerFactory.create();
	relationshipController.addCreateListener(this);
	relationshipController.addDiagram(this.getDiagram());
	relationshipController.create();
    }

    @Override
    public void createClass(String entityName) {
	if (!this.hasPendingClass()) {
	    this.currentOperation = Operations.CreateClass;
	    IClassController classController = this.classControllerFactory.create();
	    classController.addSubscriber(this);
	    classController.create(entityName);
	}
    }

    @Override
    public void handleCreatedEvent(Class pendingClass) {
	switch (this.currentOperation) {
	case UpdateClass:
	    	this.handleClassUpdate(pendingClass);
	    for (IDiagramEventListener listener : this.listeners) {
		listener.handleClassUpdated(this.diagram, pendingClass);
	    }
	    break;
	case CreateClass:
	default:
	    this.pendingClass = pendingClass;
	    break;
	}
	this.currentOperation = Operations.None;
    }

    private void handleClassUpdate(Class pendingClass) {
	this.graph.getModel().beginUpdate();
	Object parent = this.graph.getDefaultParent();
	try {
	    mxCell classCell = this.getEntityCell(pendingClass.getId().toString());
	    int x = (int)classCell.getGeometry().getX();
	    int y = (int)classCell.getGeometry().getY();
		
		if( classCell != null) {
		    Object[] fafa = {classCell};
		    this.graph.removeCells(fafa);
   
		}
	    classCell = this.addClassToGraph(pendingClass, parent, x, y);	    
	    this.addAttributesToGraph(classCell, pendingClass.getAttributes(), pendingClass.getId(),
		    StyleConstants.ATTRIBUTE_HEIGHT);
	    this.addMethodToGraph(classCell, pendingClass.getMethods(), pendingClass.getId());
//	    this.graph.getView().getState(classCell).setLabel(pendingClass.getName());
		classCell.setValue(pendingClass.getName());
		this.diagramView.refreshGraphComponent();
	} catch (Exception e) {
		e.printStackTrace();
	} finally {
		this.graph.getModel().endUpdate();
		this.graph.repaint();
	}	
    }

    @Override
    public void createInterface(String entityName) {
	if (!this.hasPendingClass()) {
	    this.currentOperation = Operations.CreateInterface;
	    IInterfaceController interfaceController = this.interfaceControllerFactory.create();
	    interfaceController.addSubscriber(this);
	    interfaceController.create(entityName);
	}
    }

    @Override
    public void invoke(Object arg0, mxEventObject arg1) {
	// TODO Auto-generated method stub

    }

    public mxCell getEntityCell(String id) {
	return this.classCells.get(CellConstants.ClassPrefix + id);
    }

    public mxCell getRelationshipCell(String id) {
	return this.relationshipConectorCells.get(CellConstants.RelationshipConnectorPrefix + id);
    }

    @Override
    public void handleCreatedEvent(List<Relationship> relationships)
	    throws Exception {

	for (Relationship relationship : relationships) {
	    this.graph.getModel().beginUpdate();
	    try {
		Object parent = this.graph.getDefaultParent();
		UUID idA = relationship.getComponentA().getEntity().getId();
		UUID idB = relationship.getComponentB().getEntity().getId();
		int entityCount = 2;
		if(idA == idB)
		    entityCount = 1;
		double partialRelationshipEntitiesAngle = entityCount != 0 ? (2 * Math.PI)/entityCount : 0;
		double currentRelationshipEntitiesAngle = 0;
		
		double xExit = 0;
		double yExit = 0;
		Boolean useExit = false;
		if (entityCount == 1) {
		    xExit = Math.cos(currentRelationshipEntitiesAngle) * 0.5 + 0.5;
		    yExit = Math.sin(currentRelationshipEntitiesAngle) * 0.5 + 0.4;
		    useExit = true;
		}

		this.addRelationshipConnectorToGraph(parent, relationship, xExit, yExit, useExit, 1);
	    } finally {
		 if (this.currentOperation == Operations.CreateRelationship) {
		     this.diagram.getRelationships().add(relationship); 
		     for (IDiagramEventListener eventListener : this.listeners) {
        		  eventListener.handleRelationshipAdded(this.diagram, relationship); 
		     } 
		 } else { 
		     for (IDiagramEventListener listener: this.listeners) {
//        		  listener.handleRelationshipUpdated(this.diagram, relationship);
        	     }
		  }
		 this.graph.getModel().endUpdate();
	    }

	    this.currentOperation = Operations.None;
	}
    }

    private mxCell addRelationshipConnectorToGraph(Object parent, Relationship relationship, double exitX, double exitY,
	    Boolean useExit, Integer appeareance) {
	
	RelationshipComponent relationshipComponentA = relationship.getComponentA();
	RelationshipComponent relationshipComponentB = relationship.getComponentB();
	String cardinalityDisplayA = "";
	String cardinalityDisplayB = ""; 
	
	if(!relationship.getType().equals(RelationshipTypes.Inheritance)){
	    String maxCard = "";
	    if (relationshipComponentA.getMaxCardinality() != null)
		maxCard = relationshipComponentA.getMaxCardinality().toString();
	    String minCard = "";
	    if (relationshipComponentA.getMinCardinality() != null)
		minCard = relationshipComponentA.getMinCardinality().toString();
	    if(minCard != "" && maxCard != "")
		cardinalityDisplayA = String.format("(%s,%s)  ", maxCard, minCard);

	    maxCard = "";
	    if (relationshipComponentB.getMaxCardinality() != null)
		maxCard = relationshipComponentB.getMaxCardinality().toString();
    	    minCard = "";
    	    if (relationshipComponentB.getMinCardinality() != null)
        	minCard = relationshipComponentB.getMinCardinality().toString();
    	    if(minCard != "" && maxCard != "")
    		cardinalityDisplayB = String.format("(%s,%s)  ", maxCard, minCard);
    	}
	
	mxCell entityCellA = this.getEntityCell(relationshipComponentA.getEntity().getId().toString());
	mxCell entityCellB = this.getEntityCell(relationshipComponentB.getEntity().getId().toString());
	
	String exitStyle = "";

	if (useExit) {
	    exitStyle = ";" + Styler.getEdgeExitStyle(exitX, exitY);
	}
	mxCell existingCell = this.getRelationshipCell(relationship.getId().toString());
	if( existingCell != null) {
	    Object[] fafa = {existingCell};
	    this.graph.removeCells(fafa);
	}
	mxCell connectorCell = (mxCell) this.graph.insertEdge(parent,
		CellConstants.RelationshipConnectorPrefix + relationship.getId(),
		cardinalityDisplayA + cardinalityDisplayB, entityCellA, entityCellB,
		relationship.getLinkName()  + exitStyle);

	this.relationshipConectorCells.put(
		CellConstants.RelationshipConnectorPrefix + relationship.getId(),
		connectorCell);

	return connectorCell;
    }

    @Override
    public void save() throws ParserConfigurationException {
	Document document = this.xmlFileManager.createDocument();
	Element element = this.diagramXmlManager.getElementFromItem(
			this.diagram, document);

	document.appendChild(element);
	this.xmlFileManager.write(document, this.getComponentFilePath());

	this.graphPersistenceService.save(this.getRepresentationFilePath(),
			this.graph);
    }
    
    private String getRepresentationFilePath() {
	return this.projectContext.getDataDirectory() + "/"
				+ this.diagram.getName() + "-rep";
    }

    private String getComponentFilePath() {
	return this.projectContext.getDataDirectory() + "/"
				+ this.diagram.getName() + "-comp";
    }

    @Override
    public void load(Diagram diagram) {
	this.diagram = diagram;
	String fileName = this.getRepresentationFilePath();
	this.graphPersistenceService.load(fileName, this.graph);
	Pattern regex1 = Pattern.compile(CellConstants.ClassPrefix);
	Pattern regex2 = Pattern.compile(CellConstants.RelationshipConnectorPrefix);
	for (Object o : this.graph.getChildCells(this.graph.getDefaultParent())) {
		mxCell cell = (mxCell) o;

		Matcher matcher = regex1.matcher(cell.getId());
		boolean matchFound = matcher.find();
		if (matchFound) {
			this.classCells.put(cell.getId(), cell);
			continue;
		}
		matcher = regex2.matcher(cell.getId());
		matchFound = matcher.find();
		if (matchFound) {
			this.relationshipConectorCells.put(cell.getId(), cell);
			continue;
		}
	}
	
    }

    @Override
    public void update(Class entity) {
	this.currentOperation = Operations.UpdateClass;
	IClassController classController = this.classControllerFactory.create();
	classController.addSubscriber(this);
	classController.create(entity);
	
    }
    
    @Override
    public void update(Interface entity) {
	this.currentOperation = Operations.UpdateInterface;
	IInterfaceController interfaceController = this.interfaceControllerFactory.create();
	interfaceController.addSubscriber(this);
	interfaceController.create(entity);
    }
    
    @Override
    public void update(Relationship relationship) {
	this.currentOperation = Operations.UpdateRelationship;
	IRelationshipController relationshipController = this.relatControllerFactory.create();
	relationshipController.addCreateListener(this);
	relationshipController.setDiagram(diagram);
	relationshipController.createRelationship(relationship);
    }

    @Override
    public void handleCreatedEvent(Interface pendingInterface) {
	switch (this.currentOperation) {
	case UpdateInterface:
	    // this.handleEntityUpdate(pendingClass);
	    // this.diagram.isNotValidated();
	    for (IDiagramEventListener listener : this.listeners) {
		// listener.handleEntityUpdated(this.diagram, pendingClass);
	    }
	    break;
	case CreateInterface:
	default:
	    this.pendingInterface = pendingInterface;
	    break;
	}
	this.currentOperation = Operations.None;
	
    }

    @Override
    public boolean hasPendingInterface() {
	return this.pendingInterface != null;
    }

    @Override
    public void addInterface(int x, int y) throws Exception {
	if (pendingInterface == null) {
	    return;
	}
	this.graph.getModel().beginUpdate();
	Object parent = this.graph.getDefaultParent();
	try {
	    mxCell interfaceCell = this.addInterfaceToGraph(this.pendingInterface, parent, x, y);
	    this.addAttributesToGraph(interfaceCell,null,  this.pendingInterface.getId(), StyleConstants.ATTRIBUTE_INTERFACE_HEIGHT);
	    this.addMethodToGraph(interfaceCell, this.pendingInterface.getMethods(),this.pendingInterface.getId());
	} finally {
	    this.diagram.getEntities().add(this.pendingInterface);
	    // this.diagram.isNotValidated();
	    for (IDiagramEventListener listener : this.listeners) {
		listener.handleInterfaceAdded(this.diagram, this.pendingInterface);
	    }
	    this.graph.getModel().endUpdate();
	}
	this.pendingInterface= null;
    }

    private mxCell addInterfaceToGraph(Interface pendInterface, Object parent, int x, int y)
	    throws Exception {
	
	String name = "<< Interface >> \n" + pendInterface.getName();  
	mxCell classCell = (mxCell) this.graph.insertVertex(parent,
		CellConstants.InterfacePrefix + pendInterface.getId().toString(),
		name, x, y, StyleConstants.INTERFACE_WIDTH,
		StyleConstants.INTERFACE_HEIGHT, StyleConstants.INTERFACE_STYLE);
	
	this.classCells.put(CellConstants.ClassPrefix
		+ pendInterface.getId().toString(), classCell);

	return classCell;
    }

    @Override
    public void createSubDiagram(String diagramName) {
	for (IDiagramEventListener listener : this.listeners) {
	    listener.handleSubDiagramCreated(this.diagram, diagramName);
	}
	try {
	    this.save();
	} catch (Exception e) {
	    e.printStackTrace();
	}
    }

    @Override
    public boolean deleteRelationship(Relationship relationship) {
	boolean exists = false;
	for(Relationship rel : this.diagram.getRelationships()){
	    if (rel.getId().equals(relationship.getId()))
		exists = true;
	}
	if(!exists){
	    this.diagramView.showDeleteDialog("relationship " + relationship.toString(),
		    " don't belong into current diagram", false);
	    return false;
	}
	
	if (this.diagramView.showDeleteDialog("relationship " + relationship.toString(), "", true)) {
		this.removeRelationship(relationship);
		return true;
	}
	return false;
    }

    private void removeRelationship(Relationship relationship) {
	mxIGraphModel model = this.graph.getModel();
	String relationshipCell = CellConstants.RelationshipConnectorPrefix + relationship.getId().toString();
	model.remove(this.relationshipConectorCells.remove(relationshipCell));
	try {
	    this.diagram.getRelationships().remove(relationship);
	} catch (Exception e) {
		e.printStackTrace();
	}
	
    }

    @Override
    public boolean deleteEntity(Entity entity) {
	boolean exists = false;
	for(Entity e : this.diagram.getEntities()){
	    if (e.getId().equals(entity.getId()))
		exists = true;
	}
	if(!exists){
	    this.diagramView.showDeleteDialog("entity " + entity.getName(),
        		" don't belong into current diagram", false);
	    return false;
	}
	if (canDeleteEntity(entity)) {
		if (this.diagramView.showDeleteDialog("entity " + entity.getName(),"", true)) {
			this.removeEntity(entity);
			return true;
		}
	} else {
		this.diagramView.showDeleteDialog("entity " + entity.getName()," belongs to a relationship", false);
	}
	return false;
    }
    
    private void removeEntity(Entity entity) {
	mxIGraphModel model = this.graph.getModel();
	String entityCell = CellConstants.ClassPrefix 	+ entity.getId().toString();
	model.remove(this.classCells.remove(entityCell));
	this.diagram.getEntities().remove(entity);

    }

    private boolean canDeleteEntity(Entity entity) {
	for (Relationship relationship : this.diagram.getRelationships()){
	    if(relationship.getComponentA().getEntity().getId().equals(entity.getId())
		    || relationship.getComponentB().getEntity().getId().equals(entity.getId()))
		return false;
	}
	return true;
    }

    @Override
    public boolean validateClassName(String className) {
	for(Entity entity : this.diagram.getEntities()){
	    if(entity.getName().equals(className))
		return false;
	}
	return true;
    }

    @Override
    public void addAttributeToConvertedGraph() {
	for(Entity e : this.diagram.getEntities()){
	    mxCell classCell =  this.getEntityCell(e.getId().toString());
	    if(e instanceof Class)
		this.addAttributesToGraph(classCell, ((Class) e).getAttributes(),
		    e.getId(), StyleConstants.ATTRIBUTE_HEIGHT);
	}
	
    }

    @Override
    public void removeListener() {
	listeners.clear();
    }
   
    
}
